unit fHTFpackD;

interface

uses
  Winapi.Windows,
  System.SysUtils,
  System.Classes,
  System.Actions,
  System.Math,
  System.ImageList,
  Vcl.Graphics,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.Dialogs,
  ValEdit,
  Vcl.Grids,
  Vcl.Menus,
  Vcl.StdCtrls,
  Vcl.ComCtrls,
  Vcl.ToolWin,
  Vcl.ExtCtrls,
  Vcl.ActnList,
  Vcl.ImgList,

  GLS.HeightTileFileHDS;

type
  TSrc = record
    fs: TFileStream;
    x, y, w, h: Integer;
    format: Integer;
    FlipRotate: Integer;
  end;

  PSrc = ^TSrc;

  TMainForm = class(TForm)
    MainMenu: TMainMenu;
    StringGrid: TStringGrid;
    File1: TMenuItem;
    ActionList: TActionList;
    ImageList: TImageList;
    ACOpen: TAction;
    ACSave: TAction;
    ACExit: TAction;
    Open1: TMenuItem;
    Save1: TMenuItem;
    N1: TMenuItem;
    Exit1: TMenuItem;
    Panel1: TPanel;
    Label1: TLabel;
    Label2: TLabel;
    EDHTFName: TEdit;
    EDDEMPath: TEdit;
    BUDEMPath: TButton;
    BUPickHTF: TButton;
    ToolBar: TToolBar;
    ToolButton1: TToolButton;
    ToolButton2: TToolButton;
    ToolButton3: TToolButton;
    DEMs1: TMenuItem;
    ACNewDEM: TAction;
    ACRemoveDEM: TAction;
    ToolButton4: TToolButton;
    ToolButton5: TToolButton;
    AddDEMsource1: TMenuItem;
    RemoveDEMsource1: TMenuItem;
    SDHTF: TSaveDialog;
    PopupMenu: TPopupMenu;
    AddDEMsource2: TMenuItem;
    RemoveDEMsource2: TMenuItem;
    MIAbout: TMenuItem;
    CBType: TComboBox;
    CBFile: TComboBox;
    Label3: TLabel;
    EDSizeX: TEdit;
    EDSizeY: TEdit;
    Label4: TLabel;
    Label5: TLabel;
    EDDefaultZ: TEdit;
    ODTerrainPack: TOpenDialog;
    SDTerrainPack: TSaveDialog;
    ToolButton6: TToolButton;
    ACProcess: TAction;
    ToolButton7: TToolButton;
    N2: TMenuItem;
    Process1: TMenuItem;
    PanelFoot: TPanel;
    ProgressBar: TProgressBar;
    EDTileSize: TEdit;
    Label6: TLabel;
    ToolButton8: TToolButton;
    ACViewer: TAction;
    N3: TMenuItem;
    HTFViewer1: TMenuItem;
    ToolButton9: TToolButton;
    ODPath: TOpenDialog;
    Label7: TLabel;
    EDTileOverlap: TEdit;
    Label8: TLabel;
    EDZFilter: TEdit;
    Label9: TLabel;
    EDZScale: TEdit;
    CBWholeOnly: TCheckBox;
    CBFlipRotate: TComboBox;
    procedure ACExitExecute(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure BUDEMPathClick(Sender: TObject);
    procedure BUPickHTFClick(Sender: TObject);
    procedure MIAboutClick(Sender: TObject);
    procedure ActionListUpdate(Action: TBasicAction; var Handled: Boolean);
    procedure ACNewDEMExecute(Sender: TObject);
    procedure ACRemoveDEMExecute(Sender: TObject);
    procedure StringGridSelectCell(Sender: TObject; ACol, ARow: Integer;
      var CanSelect: Boolean);
    procedure CBTypeChange(Sender: TObject);
    procedure ACSaveExecute(Sender: TObject);
    procedure ACOpenExecute(Sender: TObject);
    procedure EDDEMPathChange(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure EDDefaultZChange(Sender: TObject);
    procedure ACProcessExecute(Sender: TObject);
    procedure ACViewerExecute(Sender: TObject);
    procedure EDZFilterChange(Sender: TObject);
    procedure EDZScaleChange(Sender: TObject);
  private

    sources: array of TSrc;
    defaultZ: SmallInt;
    filterZ: SmallInt;
    zScale: Single;

    procedure Parse;
    procedure Cleanup;
    procedure SrcExtractFlip(src: PSrc; relX, relY, len: Integer;
      dest: PSmallInt);
    procedure SrcExtract(src: PSrc; relX, relY, len: Integer; dest: PSmallInt;
      DiagFlip: Boolean = false);
    procedure WorldExtract(x, y, len: Integer; dest: PSmallInt);

  public

  end;

var
  MainForm: TMainForm;

implementation

uses
  fViewerD;

{$R *.dfm}

procedure TMainForm.FormCreate(Sender: TObject);
var
  i: Integer;
begin
  with ActionList do
    for i := 0 to ActionCount - 1 do
      with TAction(Actions[i]) do
        Hint := Caption;
  with StringGrid do
  begin
    Cells[0, 0] := 'File Name';
    ColWidths[0] := 140;
    Cells[1, 0] := 'World Offset';
    ColWidths[1] := 80;
    Cells[2, 0] := 'Size (rotated)';
    ColWidths[2] := 80;
    Cells[3, 0] := 'Data type';
    ColWidths[3] := 120;
    Cells[4, 0] := 'Flip and Rotate';
    ColWidths[4] := 110;
    Row := 0;
  end;
  zScale := 1;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  Cleanup;
end;

procedure TMainForm.ACExitExecute(Sender: TObject);
begin
  Close;
end;

procedure TMainForm.BUDEMPathClick(Sender: TObject);
begin
  ODPath.InitialDir := EDDEMPath.Text;
  ODPath.FileName := EDDEMPath.Text + 'pick a dummy.file';
  if ODPath.Execute then
    EDDEMPath.Text := ExtractFilePath(ODPath.FileName);
end;

procedure TMainForm.BUPickHTFClick(Sender: TObject);
begin
  SDHTF.FileName := EDHTFName.Text;
  if SDHTF.Execute then
    EDHTFName.Text := SDHTF.FileName;
end;

procedure TMainForm.MIAboutClick(Sender: TObject);
begin
  ShowMessage(Caption + #13#10#13#10 + 'HTF Generation Utility'#13#10 +
    'Part of GLScene library.'#13#10#13#10 + 'http://glscene.org');
end;

procedure TMainForm.ActionListUpdate(Action: TBasicAction;
  var Handled: Boolean);
begin
  ACRemoveDEM.Enabled := (StringGrid.RowCount > 2);
end;

procedure TMainForm.ACNewDEMExecute(Sender: TObject);
begin
  StringGrid.RowCount := StringGrid.RowCount + 1;
end;

procedure TMainForm.ACRemoveDEMExecute(Sender: TObject);
var
  i: Integer;
begin
  with StringGrid do
  begin
    i := Row;
    if i < RowCount - 1 then
    begin
      while i < RowCount - 1 do
      begin
        Rows[i] := Rows[i + 1];
        Inc(i);
      end;
    end
    else
      Row := i - 1;
    RowCount := RowCount - 1;
  end;
end;

procedure TMainForm.StringGridSelectCell(Sender: TObject; ACol, ARow: Integer;
  var CanSelect: Boolean);

  procedure SetCB(const cb: TComboBox);
  var
    r: TRect;
    i: Integer;
  begin
    r := StringGrid.CellRect(ACol, ARow);
    cb.Left := r.Left + StringGrid.Left;
    cb.Top := r.Top + StringGrid.Top;
    cb.Width := r.Right + 1 - r.Left;
    i := cb.Items.IndexOf(StringGrid.Cells[ACol, ARow]);
    if i >= 0 then
      cb.ItemIndex := i
    else
      cb.Text := StringGrid.Cells[ACol, ARow];
    if Visible then
      cb.SetFocus;
  end;

begin
  if ARow > 0 then
  begin
    if ACol = 0 then
    begin
      CBFile.Visible := True;
      SetCB(CBFile);
    end
    else
      CBFile.Visible := false;
    if ACol = 3 then
    begin
      CBType.Visible := True;
      SetCB(CBType);
    end
    else
      CBType.Visible := false;
    if ACol = 4 then
    begin
      CBFlipRotate.Visible := True;
      SetCB(CBFlipRotate);
    end
    else
      CBFlipRotate.Visible := false;
    CanSelect := True;
  end;
end;

procedure TMainForm.CBTypeChange(Sender: TObject);
begin
  with StringGrid do
    Cells[Col, Row] := (Sender as TComboBox).Text;
end;

procedure TMainForm.ACSaveExecute(Sender: TObject);
var
  i: Integer;
  sl, sg: TStringList;
begin
  if SDTerrainPack.Execute then
  begin
    sl := TStringList.Create;
    with sl do
    begin
      Values['HTFName'] := EDHTFName.Text;
      Values['WorldSizeX'] := EDSizeX.Text;
      Values['WorldSizeY'] := EDSizeY.Text;
      Values['TileSize'] := EDTileSize.Text;
      Values['TileOverlap'] := EDTileOverlap.Text;
      Values['DefaultZ'] := EDDefaultZ.Text;
      Values['FilterZ'] := EDZFilter.Text;
      Values['ZScale'] := EDZScale.Text;
      Values['DEMPath'] := EDDEMPath.Text;
      Values['WholeTiles'] := IntToStr(Integer(CBWholeOnly.Checked));
      sg := TStringList.Create;
      for i := 1 to StringGrid.RowCount - 1 do
        sg.Add(StringGrid.Rows[i].CommaText);
      Values['DEMs'] := sg.CommaText;
      sg.Free;
    end;
    sl.SaveToFile(SDTerrainPack.FileName);
    sl.Free;
  end;
end;

procedure TMainForm.ACOpenExecute(Sender: TObject);
var
  i: Integer;
  sl, sg: TStringList;
begin
  if ODTerrainPack.Execute then
  begin
    sl := TStringList.Create;
    sl.LoadFromFile(ODTerrainPack.FileName);
    with sl do
    begin
      EDHTFName.Text := Values['HTFName'];
      EDSizeX.Text := Values['WorldSizeX'];
      EDSizeY.Text := Values['WorldSizeY'];
      EDTileSize.Text := Values['TileSize'];
      EDTileOverlap.Text := Values['TileOverlap'];
      EDDefaultZ.Text := Values['DefaultZ'];
      EDZFilter.Text := Values['FilterZ'];
      EDZScale.Text := Values['ZScale'];
      EDDEMPath.Text := Values['DEMPath'];
      CBWholeOnly.Checked := (Values['WholeTiles'] = '1');
      sg := TStringList.Create;
      sg.CommaText := Values['DEMs'];
      StringGrid.RowCount := sg.Count + 1;
      for i := 0 to sg.Count - 1 do
        StringGrid.Rows[i + 1].CommaText := sg[i];
      sg.Free;
    end;
    sl.Free;
    SDTerrainPack.FileName := ODTerrainPack.FileName;
  end;
end;

procedure TMainForm.EDDEMPathChange(Sender: TObject);
var
  f: TSearchRec;
  r: Integer;
begin
  CBFile.Items.Clear;
  r := FindFirst(EDDEMPath.Text + '\*.*', faAnyFile, f);
  while r = 0 do
  begin
    if (f.Attr and faDirectory) = 0 then
      CBFile.Items.Add(f.Name);
    r := FindNext(f);
  end;
  FindClose(f);
end;

procedure TMainForm.EDDefaultZChange(Sender: TObject);
begin
  defaultZ := StrToIntDef(EDDefaultZ.Text, 0);
  if EDZFilter.Text = '' then
    filterZ := defaultZ;
end;

procedure TMainForm.EDZFilterChange(Sender: TObject);
begin
  filterZ := StrToIntDef(EDZFilter.Text, defaultZ);
end;

procedure TMainForm.EDZScaleChange(Sender: TObject);
begin
  zScale := StrToFloatDef(EDZScale.Text, 1.0);
end;

procedure TMainForm.Parse;
var
  i, p: Integer;
  Row: TStrings;
begin
  Cleanup;
  SetLength(sources, StringGrid.RowCount - 1);
  for i := 0 to High(sources) do
  begin
    Row := StringGrid.Rows[i + 1];
    sources[i].fs := TFileStream.Create(EDDEMPath.Text + '\' + Row[0],
      fmOpenRead + fmShareDenyNone);
    p := Pos(',', Row[1]);
    sources[i].x := StrToInt(Copy(Row[1], 1, p - 1));
    sources[i].y := StrToInt(Copy(Row[1], p + 1, MaxInt));
    p := Pos('x', Row[2]);
    sources[i].w := StrToInt(Copy(Row[2], 1, p - 1));
    sources[i].h := StrToInt(Copy(Row[2], p + 1, MaxInt));
    sources[i].format := CBType.Items.IndexOf(Row[3]); // File format
    sources[i].FlipRotate := CBFlipRotate.Items.IndexOf(Row[4]);
    // Flip and Rotate
  end;
end;

procedure TMainForm.Cleanup;
var
  i: Integer;
begin
  for i := 0 to High(sources) do
    sources[i].fs.Free;
  SetLength(sources, 0);
end;

procedure TMainForm.SrcExtractFlip(src: PSrc; relX, relY, len: Integer;
  dest: PSmallInt);
var
  i: Integer;
  val: SmallInt;
begin
  if src.FlipRotate <= 0 then
    SrcExtract(src, relX, relY, len, dest) // None
  else
  begin
    for i := 0 to len - 1 do
    begin
      case src.FlipRotate of
        // 0 : SrcExtract(src,relX, relY+i,1,@val);                   //No change                  (    )
        1:
          SrcExtract(src, src.w - (relX + i), relY, 1, @val);
          // H-Flip                     (Flip)
        2:
          SrcExtract(src, relY, src.w - (relX + i), 1, @val, True);
          // DiagFlip + H-Flip          (90deg)
        3:
          SrcExtract(src, src.w - (relX + i), src.h - relY, 1, @val);
          // H-Flip   + V-Flip          (180deg)
        4:
          SrcExtract(src, src.h - relY, (relX + i), 1, @val, True);
          // DiagFlip + V-Flip          (270deg)
        5:
          SrcExtract(src, src.h - relY, src.w - (relX + i), 1, @val, True);
          // DiagFlip + V-Flip + H-Flip (Flip-90deg)
        6:
          SrcExtract(src, relX + i, src.h - relY, 1, @val);
          // V-FLIP                     (Flip-180deg)
        7:
          SrcExtract(src, relY, relX + i, 1, @val, True);
          // DiagFlip                   (Flip-270deg)
      end;
      PSmallIntArray(dest)[i] := val;
    end;
  end;
end;

procedure TMainForm.SrcExtract(src: PSrc; relX, relY, len: Integer;
  dest: PSmallInt; DiagFlip: Boolean = false);
var
  i, c: Integer;
  wd: Word;
  buf: array of Single;
  bmp: TBitmap;
  rw: Integer; // rotated width
begin
  if DiagFlip then
    rw := src.h
  else
    rw := src.w;

  with src^ do
  begin
    case format of
      0:
        begin // 16bits Intel
          fs.Position := (relX + relY * rw) * 2;
          fs.Read(dest^, len * 2);
        end;
      1:
        begin // 16bits unsigned Intel
          fs.Position := (relX + relY * rw) * 2;
          fs.Read(dest^, len * 2);
          for i := 0 to len - 1 do
          begin
            wd := PWord(Integer(dest) + i * 2)^;
            PSmallInt(Integer(dest) + i * 2)^ := Integer(wd) - 32768;
          end;
        end;
      2:
        begin // 16bits non-Intel
          fs.Position := (relX + relY * rw) * 2;
          fs.Read(dest^, len * 2);
          for i := 0 to len - 1 do
          begin
            wd := PWord(Integer(dest) + i * 2)^;
            PWord(Integer(dest) + i * 2)^ := ((wd and 255) shl 8) + (wd shr 8);
          end;
        end;
      3:
        begin // VTP's BT single
          fs.Position := (relX + relY * rw) * 4 + 256;
          SetLength(buf, len);
          fs.Read(buf[0], len * 4);
          for i := 0 to len - 1 do
            PSmallInt(Integer(dest) + i * 2)^ := Round(buf[i]);
        end;
      4:
        begin // windows BMP
          bmp := TBitmap.Create;
          try
            fs.Position := 0;
            bmp.LoadFromStream(fs);
            if DiagFlip then
              rw := bmp.Width
            else
              rw := bmp.Height;
            for i := 0 to len - 1 do
            begin
              c := bmp.Canvas.Pixels[relX + i, rw - relY - 1];
              PSmallInt(Integer(dest) + i * 2)^ := (GetGValue(c) - 128) shl 7;
            end;
          finally
            bmp.Free;
          end;
        end;
      5:
        begin // 32bits FP Intel
          fs.Position := (relX + relY * rw) * 4;
          SetLength(buf, len);
          fs.Read(buf[0], len * 4);
          for i := 0 to len - 1 do
            PSmallInt(Integer(dest) + i * 2)^ := Round((buf[i] - 0.5) * 32000);
        end;
      6:
        begin // DTED
          fs.Position := 3434 + (relX + relY * rw) * 2 + (relY * 12);
          fs.Read(dest^, len * 2);
          for i := 0 to len - 1 do
          begin
            wd := PWord(Integer(dest) + i * 2)^;
            PWord(Integer(dest) + i * 2)^ := ((wd and 255) shl 8) + (wd shr 8);
          end;
        end;
    end;
  end;
end;

procedure TMainForm.WorldExtract(x, y, len: Integer; dest: PSmallInt);
var
  i, n, rx, ry: Integer;
  src: PSrc;
begin
  while len > 0 do
  begin
    src := nil;
    for i := 0 to High(sources) do
    begin
      if (sources[i].x <= x) and (sources[i].y <= y) and
        (x < sources[i].x + sources[i].w) and (y < sources[i].y + sources[i].h)
      then
      begin
        src := @sources[i];
        Break;
      end;
    end;
    if Assigned(src) then
    begin
      rx := x - src.x;
      ry := y - src.y;
      n := len;
      if rx + n > src.w then
        n := src.w - rx;
      SrcExtractFlip(src, rx, ry, n, dest);
      if filterZ <> defaultZ then
      begin
        for i := 0 to n - 1 do
          if PSmallIntArray(dest)[i] = filterZ then
            PSmallIntArray(dest)[i] := defaultZ;
      end;
      if zScale <> 1 then
      begin
        for i := 0 to n - 1 do
          PSmallIntArray(dest)[i] := Round(PSmallIntArray(dest)[i] * zScale);
      end;
      Dec(len, n);
      Inc(dest, n);
      Inc(x, n);
    end
    else
    begin
      dest^ := defaultZ;
      Inc(dest);
      Dec(len);
      Inc(x);
    end;
  end;
end;

procedure TMainForm.ACProcessExecute(Sender: TObject);
var
  x, y, wx, wy, ts, tx, ty, i, j, overlap: Integer;
  n, maxN: Cardinal;
  htf: TGLHeightTileFile;
  buf: array of SmallInt;
  f: file of Byte;
begin
  Screen.Cursor := crHourGlass;

  wx := StrToInt(EDSizeX.Text);
  wy := StrToInt(EDSizeY.Text);
  ts := StrToInt(EDTileSize.Text);
  overlap := StrToInt(EDTileOverlap.Text);
  Parse;
  SetLength(buf, ts * ts);
  htf := TGLHeightTileFile.CreateNew(EDHTFName.Text, wx, wy, ts);
  htf.defaultZ := defaultZ;
  ProgressBar.Max := 1000;
  maxN := Ceil(wx / ts) * Ceil(wy / ts);
  n := 0;
  ProgressBar.Position := 0;
  y := 0;
  while y < wy do
  begin
    ty := wy + overlap - y;
    if ty > ts then
      ty := ts;
    x := 0;
    while x < wx do
    begin
      tx := wx + overlap - x;
      if (not CBWholeOnly.Checked) or ((tx >= ts) and ((wy - y) >= ts)) then
      begin
        if tx > ts then
          tx := ts;
        for i := 0 to ty - 1 do
        begin
          WorldExtract(x, y + i, tx, @buf[i * ts]);
          if overlap > 0 then
          begin
            for j := tx to ts - 1 do
              buf[i * ts + j] := buf[i * ts + tx - 1];
          end
          else
          begin
            for j := tx to ts - 1 do
              buf[i * ts + j] := defaultZ;
          end;
        end;
        if overlap > 0 then
        begin
          for i := ty to ts - 1 do
            for j := 0 to ts - 1 do
              buf[i * ts + j] := buf[(i - 1) * ts + j];
        end
        else
        begin
          for i := ty to ts - 1 do
            for j := 0 to ts - 1 do
              buf[i * ts + j] := defaultZ;
        end;
        htf.CompressTile(x, y, ts, ts, @buf[0]);
      end;
      Inc(x, ts - overlap);
      Inc(n);
      ProgressBar.Position := (n * 1000) div maxN;
      if (n and 15) = 0 then
      begin
        Application.ProcessMessages;
      end;
    end;
    Inc(y, ts - overlap);
  end;
  htf.Free;
  Cleanup;

  Screen.Cursor := crDefault;

  AssignFile(f, EDHTFName.Text);
  Reset(f);
  i := FileSize(f);
  CloseFile(f);

  ShowMessage('HTF file created.'#13#10#13#10 + IntToStr(i) +
    ' bytes in file'#13#10 + '(' + IntToStr(wx * wy * 2) + ' raw bytes)');
end;

procedure TMainForm.ACViewerExecute(Sender: TObject);
var
  viewer: TViewerForm;
begin
  viewer := TViewerForm.Create(nil);
  try
    viewer.htf := TGLHeightTileFile.Create(EDHTFName.Text); // R
    viewer.Caption := 'HTFViewer - ' + ExtractFileName(EDHTFName.Text); // R

    viewer.ShowModal;
  finally
    viewer.Free;
  end;
end;

end.
